/*
 * Copyright (C) 2009 Leandro de Oliveira Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.japa.repository.collection.jpa;

import java.util.*;

import org.springframework.dao.*;
import org.springframework.orm.jpa.*;

/**
 * JPA-based persistent collection strategy supporting extent with
 * {@link com.google.code.japa.annotation.FetchStrategy#ONDEMAND}.
 * 
 * @param <T> Type of the entity whose strategy manipulates the extent.
 * 
 * @author Leandro Aparecido
 * @since 1.0
 * @see com.google.code.japa.annotation.FetchStrategy#ONDEMAND
 * @see com.google.code.japa.repository.collection.PersistentCollectionStrategy
 * @see com.google.code.japa.repository.collection.jpa.AbstractJpaPersistentCollectionStrategy
 * @see com.google.code.japa.repository.collection.jpa.JpaPersistentCollectionStrategyFactory
 */
public class FetchOnDemandJpaPersistentCollectionStrategy<T> extends AbstractJpaPersistentCollectionStrategy<T> {

	private static final long serialVersionUID = 1942533817841924845L;
	
	/**
	 * Retrieved entities since last flush.
	 */
	private Map<Integer, T> entities;
	
	/**
	 * Number of entities in the extent.
	 */
	private int size;

	/**
	 * Positions of the entities added since the last flush.
	 */
	private List<Integer> addedIndexes;

	/**
	 * Creates the strategy with the specified parameters.
	 * 
	 * @param entityType Type of the entity whose strategy manipulates the extent.
	 * @param jpaOperations Helper to execute JPA operations.
	 */
	public FetchOnDemandJpaPersistentCollectionStrategy(Class<T> entityType, JpaOperations jpaOperations) {
		super(entityType, jpaOperations);
		
		this.entities = new HashMap<Integer, T>();
		this.size = count();
		
		this.addedIndexes = new ArrayList<Integer>();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void doAdd(int index, T entity) {
		verifyBounds(index);
		
		T aux = entity;
		int indexToUse = index;
		do {
			aux = entities.put(indexToUse++, aux);
		} while (aux != null);

		addedIndexes.add(index);
		size++;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doGet(int index) {
		verifyBounds(index);
		
		if (!entities.containsKey(index)) {
			List<T> list = select(calculateIndexToSelect(index), 1);
			if (list.isEmpty()) {
				throw new EmptyResultDataAccessException("Could not retrive instance [" + index + "] of entity type [" + getEntityType() + "]. The database must have changed since the time of creation of the collection.", index);
			}
			entities.put(index, list.get(0));
		}
		
		return entities.get(index);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doSet(int index, T entity) {
		verifyBounds(index);
		
		T aux = doGet(index);
		entities.put(index, entity);
		
		return aux;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doRemove(int index) {
		verifyBounds(index);
		
		T aux = doGet(index);
		entities.remove(index);
		
		for (int i = index + 1; i < size; i++) {
			T temp = entities.remove(i);
			if (temp != null) {
				entities.put(i - 1, temp);
			}
		}
		addedIndexes.remove(Integer.valueOf(index));
		size--;
		
		return aux;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	protected boolean doRemove(T entity) {
		return doBufferOrSequentialRemove(entities, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected boolean doContains(T entity) {
		return doBufferOrSequentialContains(entities, entity);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	protected int doSize() {
		return size;
	}
	
	/**
	 * Clears the retrieved entities and updates extent size.
	 */
	@Override
	protected void flushed() {
		addedIndexes.clear();
		entities.clear();
		size = count();
	}

	/**
	 * Calculate the position of the next entity to retrieve.
	 * 
	 * @param index Position of the requested entity.
	 * @return Position to retrieve.
	 */
	protected int calculateIndexToSelect(int index) {
		int actualIndex = index;
		
		for (Integer addedIndex : addedIndexes) {
			if (addedIndex <= index) {
				actualIndex--;
			}
		}
		
		return actualIndex;
	}
}
